#! perl -w

package S3QL::Object;

use strict;
use warnings;
use S3QL;

my $debug = 5;

=head1 NAME

S3QL::Object - High-level object-like perl interface to the S3DB API

=head1 SYNOPSIS

 require S3QL::Object;
 S3QL::Object::setup(key => $key, host => $host);
 S3QL::Object::setup(key => $key, host => $host, projectID => $pid);
 S3QL::Object::setProject($pid);

 my @resourceIds = S3QL::Object::lookup($entity);
 my @resourceIds = S3QL::Object::lookup($entity, {$property => $value});

 my $object = new S3QL::Object({entity => $type, resource_id => $rid});
 foreach my $rid (@resourceIDs) {
    my $object = new S3QL::Object({entity =>"Folder", resource_id => $rid});
    # do something with the object ...
 }

 my @stms = $object->statements();

 $object->show;
 $object->show(\*STDOUT);
 $object->show($fh);
 $object->asXML;
 $object->asXML($fh);

 $object->save;

 S3QL::Object::debug($level);

=head1 DESCRIPTION

This class provides a high-level interface to the S3DB API described
at L<http://www.s3db.org>; the interface is built upon lower level 
structures described in the packages L<RawS3QL>  and L<S3QL>.  The main
idea is to create an object-like interface that unites a resource (or 
instance) together with all the statements about that resource into
a single perl object.

It is important to note at the outset that the C<S3QL::Object> makes a critical
simplifying assumption about the underlying semantic database.  The general
semantic web technology as implemented in S3DB is built on rules of the form 
'subject verb object', where both 'subject' and 'object' are instances of a
defined class. So, some system might include both of the following rules:

 PERSON owns CAR
 PERSON drives CAR

where the two verbs ('owns' and 'drives') represent two different relationships
between the subject (a PERSON) and the object (a CAR). Many simple (and useful)
semantic structures, however, have the property that at most one relationship
holds between any given B<type> of subject and object.  In this case, the appropriate rule
describing the relationship can be computed if you know the class of the subject
and the class of the object.  The C<S3QL::Object> package assumes that it is living
in this simpler world, which makes possible an implementation using very simple
data structures.

=head2 Setup

In order to create, read, update, or destroy objects, you must have
access to a deployment of an S3DB database where the objects live. In
order to obtain this access programmatically, you must specify the host
server and an access key.  In the C<S3QL::Object> class, you do this using
the C<setup> function:

 S3QL::Object::setup(key => $key, host => $host);
 S3QL::Object::setup(key => $key, host => $host, projectID => $pid);

For demonstration purposes, the package uses a key for a guest account
at a default host at M.D. Anderson.  You can deploy your own host by
downloading the S3DB source from L<http://www.s3db.org>.  Instructions
at the same location explain how to create user accounts and how to
create access keys.

=cut

my $demohost = 'http://ibl.mdanderson.org/s3dbdemo/';
my $guestkey = '8nEbexebSpzTUL4';
my $projectID = 109; # mystical magical project id
my $db;

sub setup {
    my %hash = ();
    while (my $key = shift) {
	$hash{$key} = shift;
	print STDERR "setup key: $key, value: $hash{$key}\n" if $debug > 6;
    }
    my $host = $hash{host} || $demohost;
    my $key = $hash{key} || $guestkey;
    if (defined($hash{projectID})) {
	$projectID = $hash{projectID};
    }
    print STDERR "using key: $key\n" if $debug > 2;
    print STDERR "using host: $host\n" if $debug > 2;
    $db = new S3QL({key => $key, host => $host});
    if ($debug > 5) {
	my $raw = $db->{raw};
	print STDERR "raw db values\n";
	foreach my $k (sort keys %$raw) {
	    print STDERR "$k => $raw->{$k}\n";
	}
    }
}

=pod

The items stored in an S3DB database are organized into projects. The
underlying model used by C<S3QL::Object> assumes that you are only 
interested in the objects from a single project.  You can optionally
supply the numerical identifier of the project to the C<setup> function.
Alternatively, you can specify the project ID later using the
C<setProject> function:

 S3QL::Object::setProject($pid);

You can either use the web interface to your deployment of S3DB to
determine the project ID, or you can use a C<select> statement in the
L<S3QL> package to get a list of project names and IDs.

=cut

sub setProject {
    my $id = shift;
    if  ($id) {
	$projectID = $id;
    } else {
	warn "Missing project_id" ;
    }
    return $projectID;
}

##############################################################
# CACHE VARIABLES

# we cache the class information in three hashes
my %classNote = (); # /notes/, indexed by resource_id or class_id
my %entity    = (); # /entity/, indexed by resource_id or class_id
my %classID   = (); # /resource_id/, indexed by entity name

# we also cache all the rules
my %rules          = (); # rule_id to hash or rule components
my %subject2ruleID = (); # subject to array of rule_id
my %verb2ruleID    = (); # verb to array of rule_id
my %object2ruleID  = (); # object to array of rule_id

# we cache instances as required, one entity (i.e., class or resource)  at a time
my %instanceID   = (); # from /entity/ to array of /instance_id/
my %instanceNote = (); # from /instance_id/ to /notes/
my %statementID  = (); # from /instance_id/ to array of /statement_id/
my %statements   = (); # from /statement_id/ to hash of statement parts
##############################################################

=pod

=head2 Searching for Objects

You can use the C<lookup> function to get a list of the resource IDs of
objects of a given type.  For example, to get a list of all resources
whose S3DB class is C<'Type'>, you would use the command:

 my @resourceIds = S3QL::Object::lookup('Type');

You can also search for objects of a given type by specifying some of
their  properties. For example, suppose the S3DB project contains objects
of type C<'Folder'> or type C<'File'>, and that one of the basic rules 
in the database is C<'File' 'belongs-to' 'Folder'>.  Suppose further that
you know the resource ID (C<$fid>) of a particular C<'Folder'> and
want to find all of the C<'File'> objects in that C<'Folder'>.  Then
you would write:

 my @resourceIds = S3QL::Object::lookup('File', {'Folder' => $fid});

B<Remark:> As mentioned above, the C<S3QL::Object> package assumes that the 
types of the subject and object together fully determine the rule that
connects them, which is the only reason this syntax works to look up objects.

=cut    

sub lookup {
    my ($ent, $href) = @_;
    my @results = ();
    return @results unless $ent;
    unless (defined($instanceID{$ent})) {
	_getInstances($ent); # cache all the existing instances of this entity
    }
    unless ($href && ref($href)) { # want all of these entities
	my @inst = @{$instanceID{$ent}}; # list of instances of this kind of entity
	return @inst;
    }
    my %query = ();
    my $subj = $subject2ruleID{$ent};
    if ($subj) {
	my @subjIDs = (@$subj);
	foreach my $key (keys %$href) {
	    next if $key eq 'entity';
	    next if $key eq 'notes';
	    print STDERR "Waiting for $key\n" if $debug > 1;
	    my $obj = $object2ruleID{$key};
	    die "No rules with object equal to '$key'\n" unless $obj;
	    my @match = ();
	    foreach my $objID (@$obj) {
		foreach my $subjID (@subjIDs) {
		    push @match, $subjID if $subjID == $objID;
		}
	    }
	    if (scalar(@match) > 1) {
		die "Multiple rules with subject '$ent' and object '$key'\n";
	    } elsif (scalar(@match) == 0) {
		die "No rules with subject '$ent' and object '$key'\n";
	    } else {
	        # Now we have exactly one rule with the desired subject and object
		print STDERR "Unique rule match: $match[0]\n" if $debug > 1;
		$query{$match[0]} = $href->{$key};
	    }
	}
	# Now query is a hash with keys equal to the rule ids.  For the corresponding rules,
	# this entity is the subject, the named component is the object, and the value (in
	# query) is equal to the one specified in the argument to this function.
	foreach my $q (keys %query) {
	    print STDERR "Query: $q => $query{$q}\n" if $debug > 2;
	}

	my @inst = @{$instanceID{$ent}}; # list of instances of this kind of entity
	unless (scalar(@inst) > 0) {
	    print STDERR "No instances exist, so nothing to lookup\n" if $debug;
	    return @results ;
	}
	# Now there are some instances to check
      INSTANCE: foreach my $instance (@inst) {
	  print STDERR "Checking instance $instance...\n" if $debug > 1;
	  my @stms = @{$statementID{$instance}}; # list of statement_id for this instance
	  next INSTANCE unless scalar(@stms) > 0;
	STATEMENT: foreach my $sid (@stms) {
	    next unless $sid;
	    my $statement = $statements{$sid};
	    my $rid = $statement->{rule_id};
	    next unless $rid;
	    print STDERR "\tUsing statement $sid for rule $rid...\n" if $debug > 2;
	    unless (defined($query{$rid})) {
		print STDERR "\t\tNo query to check\n" if $debug > 4;
		next STATEMENT;
	    }
	    unless ($statement->{value} =~ /$query{$rid}/) {
		print STDERR "\t\tQuery '$query{$rid}' does not match statement value '$statement->{value}'\n" if $debug > 4;
		next INSTANCE;
	    }
	}
	  # if we fall through to here, then every statement about this instance
	  # matches a query term
	  print STDERR "$instance is a match\n" if $debug > 0;
	  push @results, $instance;
      }
    } else { # not $subj
	print STDERR "No rules with subject $ent,\n" if $debug;
    }
    return @results;
}

=pod

=head2 Reading Objects

Once you have a resource ID in hand, you can read an object from the S3DB
database. The simplest way do achieve this goal is to write

 my $object = new S3QL::Object({entity => $type, resource_id => $rid});

If you have already used the C<lookup> function to get a list of IDs, then
a common idiom is to loop over the list constructing individual objects:

 foreach my $rid (@resourceIDs) {
    my $object = new S3QL::Object({entity =>"Folder", resource_id => $rid});
    # do something with the object ...
 }

=head2 Creating Objects

You also use the C<new S3QL::Object> method to create a new object that is not
yet stored in the database. In this case, you pass in a hash that defines
the properties of the object you are trying to create:

 my $object = new S3QL::Object({entity => $type, $prop1 => $value1, ...});

B<Remark:> As mentioned above, the C<S3QL::Object> package assumes that the 
types of the subject and object together fully determine the rule that
connects them, which is the only reason this syntax works to create
objects.  Also, since this is supposed to be a new object, you should not
include a value for the C<resource_id> component of the hash.

=cut

sub new {
    my ($class, $init) = @_;
    my $self = {};
    bless $self, ref $class || $class;
    $self->init($init);
    return $self;
}

sub init {
    my ($self, $init) = @_;
    unless (ref($init)) {
	print STDERR "Overwriting init\n" if $debug > 2;
	$init = {entity => $init};
    }
    my $ent = $init->{entity};
    die "Must provide an entity to create an S3QL::Object object" unless $ent;
    $self->{entity} = $ent;

    print STDERR "S3QL::Object init\n" if $debug > 2;

    unless(scalar(keys %classID) > 0) {
	_getClasses(); # cache all the classes
	_getRules();   # cache all the rules 
    }

    unless (defined($instanceID{$ent})) {
	_getInstances($ent); # cache all the existing instances of this entity
    }

    print STDERR "cache ready\n" if $debug > 2;

    my $iid = $init->{instance_id};
    my $rid = $init->{resource_id};
    if (defined($iid)) {
	print STDERR "instance_id: $iid\n" if $debug > 2;
	if (defined($rid) && $rid != $iid) {
	    die "Contradictory resource_id ($rid) and instance_id ($iid)\n";
	}
	$rid = $iid;
    }
    if (defined($rid))  {
	print STDERR "resource_id: $rid\n" if $debug > 2;
	my $aref = $statementID{$rid};
	unless (defined($aref)) {
	    die "Unknown resource/instance id, $rid\n";
	}
	$self->{resource_id} = $rid;
	$self->{statements} = $aref;
	$self->{changed} = 0;
	my $d = $self->{data} = {};
	$d->{notes} = $instanceNote{$rid};
	foreach my $sid (@{$statementID{$rid}}) {
	    my $s = $statements{$sid};
	    $d->{$rules{$s->{rule_id}}->{object}} = $s->{value};
	}
	return $self;
    }

    my @temp = lookup($self->{entity}, $init);
    $rid = $temp[0];
    if ($rid) {
	$self->{resource_id} = $rid;
	$self->{changed} = 0;
	$self->{statements} = $statementID{$rid};
	$self->{data} = $init;
    } else {
	$self->{new} = 1;
	$self->{changed} = 1;
	$self->{data} = $init;
    }


    return $self;
}

=pod

=head2 Getting the Statements about an Object

Regardless of where the object comes from you, can get a list of all the
statements about the object by invoking

 my @stms = $object->statements();

The return value from this function is a list of hash references. Each hash
will contain values for

=over 4

=item 1. 

C<rule_id>, the numerical ID of the rule that describes the structure
of this statement.

=item 2.

C<subject>, the subject of the rule, which should match the S3DB class
of the C<$object> on which the method was invoked. (It is sometimes 
hard to keep the subjects and objects straight....)

=item 3.

C<verb>, the verb that describes the relation represented by the rule.

=item 4.

C<object>, the class of the object of the rule.

=item 5.

C<value>, the actual value taken on by the object of the rule in this
particular statement.

=item 6.

C<created_on>, the date (and time, separated by a space) when the statement
was made.

=back

=cut 

sub statements {
    my $self = shift;
    my @s = @{$self->{statements}};
    my @val = ();
    foreach my $s (@s) {
	my %hash = %{$statements{$s}};
	my $rule = $rules{$hash{rule_id}};
	foreach my $k (keys %$rule) {
	    $hash{$k} = $rule->{k};
	}
	push @val, {%hash}; 
    }
    @val;
}



=pod

=head2 Printing Objects

 $object->show;
 $object->show(\*STDOUT);
 $object->show($fh);

The C<show> method will print the object as a string. By default, the string is
printed to STDERR, which is useful in debugging. More generally, you can pass a
reference to a file handle into the function, and the string will be printed 
there instead.

B<Important Note:> Two of the keys inside the object hash are special: 
C<statements> and C<data>.  The C<statements> key is a reference to an array of
statement IDs that identify the S3DB statements that enumerate this objects
properties. The C<data> key is a reference to a hash that contains the items
that were used to initialize the object.

=cut

sub show {
    my $self = shift;
    my $fh = shift || *STDERR;
    foreach my $k (keys %$self) {
	if ($k eq 'data') {
	    foreach my $d (keys %{$self->{data}}) {
		print $fh "$d => $self->{data}->{$d}\n";
	    }
	} elsif ($k eq 'statements') {
	    print $fh "Statement IDs:", join(' ', @{$self->{statements}}), "\n";
	} else {
	    print $fh "$k => $self->{$k}\n";
	}
    }
}


=pod

=head2 Exporting as XML

 $object->asXML;
 $object->asXML($fh);

The C<asXML> method serializes the object in an XML file. As with C<show>,
the default is to print the XML to STDERR, but you can supply a file handle
if you want to send it somewhere else.

=cut

sub asXML {
    my $self = shift; # the object to serialize
    my $fh = shift || *STDERR; # the file handle to write to
    # every object has an 'entity' and 'resource_id"  element
    print $fh "<$self->{entity} id = '$self->{resource_id}'>\n";
    # the properties of an object are stored in a list of statements
    my @s = @{$self->{statements}};
    foreach my $s (@s) {
	# get the statement hash associated to each statement id
	my $href = $statements{$s};
	my $rid = $href->{rule_id};
	my $date = $href->{created_on};
	# find the rule associated with each statement
	my $rule = $rules{$rid};
	my $verb = $rule->{verb};
	my $value = $href->{value};
	if ($value =~ /<a href=\"(.*)\">(.*)$/) {
	    $value = "<a href=\""._url_encode($1)."\">$2";
	}
	print $fh "\t<$verb obj='$rule->{object}' date='$date'>$value</$verb>\n";
    }
    print $fh "\n</$self->{entity}>\n";
}

=pod

=head2 Saving Objects

 $object->save;

New objects can be saved (inserted into the S3DB database) using the C<save>
method. For efficiency, an object remembers if it is new (to the database)
and whether it has been changed since being read from the database.  The
package should allow you to save both new objects and edited objects. I<At
present, saving edited objects that were read from the database is not yet
implemented.>

=cut

sub save {
    my $self = shift;
    if (defined($self->{new}) && $self->{new}) { # do an insert
	my $class = $classID{$self->{entity}};
	die "No S3QL database object!\n" unless $db;
	$db->insert('instance', 
		    {project_id => $projectID, 
		     class_id => $class,
		     entity => $self->{entity},
		     notes => $self->{notes} || $self->{data}->{notes} || '',
		 });
	my $iid = $db->commit;
	print STDERR "Saving $iid\n" if $debug;
	foreach my $d (keys %{$self->{data}}) {
	    print STDERR "Saving statement about $d\n" if $debug;
	    next if $d eq 'entity';
	    next if $d eq 'notes';
	    my @rid = _matchRule($self->{entity}, $d);
	    $db->insert('statement', 
			{project_id => $projectID,
			 resource_id => $iid,
			 rule_id => $rid[0],
			 value => $self->{data}->{$d},
		     });
	    print STDERR $db->query, "\n" if $debug > 1;
	    my $sid = $db->commit;
	    print STDERR $sid, "\n" if $debug > 1;
	}
	return $iid;
    } elsif ($self->{changed} && $self->{resource_id}) {  # do an update/edit
	print STDERR "Edits not yet implemented...\n" if $debug;
	return $self->{resource_id};
    } elsif ($self->{resource_id}) { # nothing has changed
	print STDERR "Nothing has changed in $self->{resource_id}...\n" if $debug;
	return $self->{resource_id};
    } else { # something is wrong
	print STDERR "Inconsistent state: claims to be existing item but no resource id\n" if $debug;
	return 0;
    }
    return -1; # this (provably) cannot happen
}

# internal function used by 'save' to figure out a rule from the class of the 
# subject and object
sub _matchRule {
    my ($subj, $obj) = @_;
    print STDERR "SO: $subj .. $obj \n" if $debug > 3;
    return () unless $subj && $obj;
    my $sidref = $subject2ruleID{$subj};
    print STDERR "SID:", join(' ', @$sidref), "\n" if $debug > 3;
    return () unless $sidref;
    my $oidref = $object2ruleID{$obj};
    print STDERR "OID:", join(' ', @$oidref), "\n" if $debug > 3;
    return () unless $oidref;
    my @match = ();
    foreach my $oid (@$oidref) {
	foreach my $sid (@$sidref) {
	    push @match, $sid if $sid == $oid 
	}
    }
    return @match;
}

=head1 Uploading Files

The C<uploadFile> function is a front end to the function with the same
name in the L<S3QL> package. It takes one of the forms

 S3QL::Object::uploadFile($filename, $resourceID, $ruleID, $notes);
 S3QL::Object::uploadFile($filename, $resourceID, $ruleID, $notes, $fh);

In the latter form, you explicitly pass in a file handle from which the file
should be read, and the C<$filename> argument is simply treated as a character
string that should be used as the name of the file in the S3DB database.  If
the file handle argument is omitted, then the C<$filename> must be the actual
name of a file on the local file system that should be uploaded. This design
decision was dictated by the need to support file upload boxes in CGI web pages,
where the browser handles the upload to the web server and provides a file 
handle, and the file must then be further tranported to the S3DB server.

=cut

# undocumented function, but that may change when we start using it ...
# thus is certainly intended to be part of the documented interface at some point
sub uploadFile {
    print STDERR "uploadFile call:", join("\t", @_), "\n" if $debug;
    die "No S3QL database object!\n" unless $db;
    $db->uploadFile(@_);
    print STDERR $db->query, "\n\n" if $debug;
    $db->commit;
}

=head1 Debugging

Because of the somewhat complicated nature of this package, which builds on two 
lower levels to create an object-like interface to a semantic web database, there
are lots of places where it will (optionally) write useful information to STDERR.
You can set the level of output by

 S3QL::Object::debug($level);

Larger values of C<$level> produce more output. Once upon a time, there was a
system to the levels, but that system has succumbed to the powers of entropy. At
present, setting the level equal to 10 gets you all the debugging information
that the package provides.

=cut

sub debug {
    my $val = shift;
    if (defined($val)) {
	my $temp = $debug;
	$debug = $val;
	$val = $temp;
    } else {
	$val = $debug;
    }
    return $val;
}

=pod

=head2 Implementation Details

In order to speed things up, the package maintains a cache of the S3DB classes,
rules, instances, and statements in memory.  You can use the functions

 S3QL::Object::printClasses();
 S3QL::Object::printRules();
 S3QL::Object::printInstances();

to print a complete list of classes, rules, or instances to STDERR. There is
no separate function to print the statements, since the C<printInstances>
function prints all the statements associated with each instance. These
functions are provided primarily with a view to their usefulness in debugging
routines that use the package.

=cut

#######################################################
# FILL THE CLASS HASH
# this is the function that reads the classes/resources
sub _getClasses {
    die "No S3QL database object!\n" unless $db;
    $db->select('*', 'classes', {project_id => $projectID,});
    my @lines = split /\n/, $db->submit;
    my @headline = split /\t/, shift @lines;
    my %hash = _findColumns(\@headline, ['resource_id', 'entity','notes']);
    foreach my $line (@lines) {
	next unless $line;
	my @stuff = split /\t/, $line;
	my $id = $stuff[$hash{resource_id}];
	my $en = $stuff[$hash{entity}];
	print STDERR "_getClasses: line = $line\n" if $debug > 6;
	print STDERR "_getClasses: id = $id, en = $en\n" if $debug > 6;
	$classID{$en} =  $id;
	$entity{$id} = $en;
	$classNote{$id} = $stuff[$hash{notes}];
    }
    return @lines;
}

sub printClasses {
    foreach my $k (keys %entity) {
	print STDERR "$k => $entity{$k}\n\t $classNote{$k}\n";
    }
}

#######################################################
# FILL THE RULE HASH
# this is the function that reads the rules
sub _getRules {
    die "No S3QL database object!\n" unless $db;
    $db->select('*', 'rules', {project_id => $projectID});
    my @lines = split /\n/, $db->submit;
    my @headline = split /\t/, shift @lines;
    my @rnames = qw(rule_id subject verb object notes);
    my %colhash = _findColumns(\@headline, \@rnames);
    foreach my $line (@lines) {
	my @rule = split /\t/, $line;
	my $id      = $rule[$colhash{rule_id}];
	my $subject = $rule[$colhash{subject}];
	my $verb    = $rule[$colhash{verb}];
	my $object  = $rule[$colhash{object}];
	my $notes   = $rule[$colhash{notes}];
	_add(\%subject2ruleID, $subject, $id);
	_add(\%verb2ruleID, $verb,  $id);
	_add(\%object2ruleID, $object, $id);
	$rules{$id} = {
	    subject => $subject,
	    verb => $verb,
	    object => $object,
	    notes => $notes,
	};
    }
}

sub printRules {
    foreach my $r (sort keys %rules) {
	my $href = $rules{$r};
	print STDERR "$r => $href->{subject} $href->{verb} $href->{object}\n";
    }
}

#######################################################
# FILL THE INSTANCE AND STATEMENT HASHES

sub _getInstances {
    my $ent = shift;
    unless(scalar(keys %classID) > 0) {
	_getClasses(); # cache all the classes
	_getRules();   # cache all the rules 
    }
    print STDERR "Getting instances of $ent\n" if $debug > 2;
    my $ie = $instanceID{$ent} = [];
    die "No S3QL database object!\n" unless $db;
    $db->select('*', 'instances', {project_id => $projectID, entity => $ent});
    my @lines = split /\n/, $db->submit;
    my @headline = split /\t/, shift @lines;
    my @rnames = qw(resource_id resource_class_id notes);
    my %colhash = _findColumns(\@headline, \@rnames);
    foreach my $line (@lines) {
	my @parts = split /\t/, $line;
	my $id    = $parts[$colhash{resource_id}];
	push @$ie, $id;
	$instanceNote{$id} = $parts[$colhash{notes}];
	$statementID{$id} = [];
    }
    # so far, we have a list of instances. 
    # Now we have to get the values associated with statements for each rule.
    my $idlist = join "|", @$ie; # list of all instance IDs
    $idlist = "~'$idlist'"; # undocumented way to make (PHP) regular expressions
    $db->select('*', 'statements', {project_id => $projectID, resource_id => $idlist});
    print STDERR $db->query(), "\n" if $debug > 6;
    my @stms = split /\n/, $db->submit;
    @headline = split /\t/, shift @stms;
    print STDERR join("\t", @headline), "\n" if $debug > 5;
    @rnames = qw(resource_id statement_id rule_id value notes created_on);
    my %stmhash = _findColumns(\@headline, \@rnames);
    foreach my $s (@stms)  {
	print STDERR "STMS: $s\n\n" if $debug > 5;
	my @stuff = split /\t/, $s;
	my $sid = $stuff[$stmhash{statement_id}];
	if ($sid) {
	    my $id = $stuff[$stmhash{resource_id}];
	    print STDERR "No id for statement: $s\n" unless ($debug == 0 || $id);
	    my $stID = $statementID{$id};
	    push @$stID, $sid;
	    $statements{$sid} = {
		'resource_id' => $id,
		'rule_id' => $stuff[$stmhash{rule_id}],
		'value' => $stuff[$stmhash{value}],
		'notes' => $stuff[$stmhash{notes}],
		'created_on' => $stuff[$stmhash{created_on}],
	    }
	} else {
	    print STDERR "Empty statement id for $stmhash{statement_id}!?\n" if $debug > 1;
	}
    }
    print STDERR "Finished getting instances of $ent\n\n" if $debug > 2;
}

sub printInstances {
    foreach my $k (keys %instanceID) {
	my @a = @{$instanceID{$k}};
	print STDERR "$k => ", join(", ", @a), "\n";
	foreach my $i (@a) {
	    print STDERR "\t$i => $instanceNote{$i}\n";
	    my @s = @{$statementID{$i}};
	    print STDERR "\t$i => ", join(', ', @s), "\n";
	    foreach my $s (@s) {
		my $href = $statements{$s};
		foreach my $key (sort keys %$href) {
		    print STDERR "\t\t$key => $href->{$key}\n";
		}
	    }
	}
    }
}

##########################################################

# internal function to turn a list of desired column names into their
# numerical indices in the tab-separated-values returned by S3QL.
sub _findColumns {
    my ($aref, $bref) = @_;
    my %hash = ();
    for my $i (0..(scalar(@$aref)-1)) {
	for my $k (@$bref) {
	    $hash{$k} = $i if $aref->[$i] eq $k;
	}
    }
    return %hash;
}

# internal function to grow an anonymous array as a hash value
sub _add {
    my ($href, $key, $value) = @_;
    if (defined($href->{$key})) {
	push @{$href->{$key}}, $value;
    } else {
	$href->{$key} = [$value];
    }
}


# yet another URL encoder and decode
sub _url_encode {
    my $str = shift;
    $str =~ s/([^A-Za-z0-9])/sprintf("%%%02X", ord($1))/seg;
    $str =~ s/%20/+/g;
    $str =~ s/%2E/./g;
    return $str;
}

sub _url_decode {
    my $str = shift;
    $str =~ s/\+/ /g;
    $str =~ s/\%([A-Fa-f0-9]{2})/pack('C', hex($1))/seg;
    return $str;
}

sub link {
    my ($a, $b) = @_;
    $b = $a unless $b;
    return _url_encode("Hyperlink: <a href=$a>$b</a>");
}


return 1; # like a good module should
__END__
